#!/usr/bin/env python
import wx
from MoveList import MoveList
import os
from ChessBoard.ChessBoard import ChessBoard

def copyBitmap(bitmap):
    """ Takes a wx.Bitmap, copies onto a new empty 
        bitmap and returns the result.  """
        
    target = wx.EmptyBitmap(*(bitmap.GetSize()))
    dc = wx.MemoryDC()
    dc.SelectObject(target)
    dc.DrawBitmap(bitmap,0,0,False)
    dc.SelectObject(wx.NullBitmap)
    return target

class ChessView(wx.Window):
    """ This class handles the graphics of a chessboard. 
        It supports drag-and-drop of pieces, simple textmove
        input and move animation. """
    
    piece_files = {'p':'b_pawn', 'r':'b_rook','b':'b_bishop', 'n':'b_knight', 'q':'b_queen', 'k':'b_king', 'P':'w_pawn', 'R':'w_rook','B':'w_bishop', 'N':'w_knight', 'Q':'w_queen', 'K':'w_king'}
    def __init__(self, parent, size):
        wx.Window.__init__(self, parent, wx.ID_ANY, size = size, style=wx.FULL_REPAINT_ON_RESIZE)
        self.squaresize = 16
##        self.SetMinSize((128,128))
        self.rebuild = False
        self.flipped = False
        self.position = []
        self.field = self.makeField(size)
        self.originalPieces = self.loadPieces()
        self.pieces = self.scalePieces(self.originalPieces)
        self.board = self.setupFromFEN()
        self.onscreen = copyBitmap(self.board)
        self.dragged_piece = None
        self.animationTimer = wx.Timer(self, wx.ID_ANY)
        self.animated = []
        self.promotion = 'q'        
        
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_IDLE, self.OnIdle)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_TIMER, self.UpdateFloats, self.animationTimer)

    def Flip(self):
        """ Flips the board, and causes a redraw. """
        
        if self.flipped: self.flipped = False
        else: self.flipped = True
        self.board = self.setupFromPosition(self.position)
        self.onscreen = copyBitmap(self.board)
    
    def GetANFromScreenPos(self, screenpos):
        """ Takes an index of the square on the board as
            displayed on the screen, and calculates the
            coordinates in regular format (ie. e4). """
            
        (x,y) = screenpos
        files = ['a','b','c','d','e','f','g','h']
        ranks = ['8','7','6','5','4','3','2','1']
        boardpos = ''
        if self.flipped:
            boardpos = files[7-x] + ranks[7-y]
        else:
            boardpos = files[x] + ranks[y]
        return boardpos

    def GetScreenPosFromAN(self, fieldtext):
        """ Generates a screen position from a field coordinate. """
        
        files = {"a":0,"b":1,"c":2,"d":3,"e":4,"f":5,"g":6,"h":7}
        ranks = {"8":0,"7":1,"6":2,"5":3,"4":4,"3":5,"2":6,"1":7}
        x = files[fieldtext[0]]
        y = ranks[fieldtext[1]]
        if self.flipped:
            x = 7 - x
            y = 7 - y
        return (x,y)

    def UpdateFloats(self, event):
        """ Graphically updates the pieces that are in transit. This
            is used primarilly to update the position for animated 
            moves. """
            
        sz = self.squaresize
        dc = wx.MemoryDC()
        dc.SelectObject(self.onscreen)
        
        if self.dragged_piece:
            self.ClearRect(self.dragged_piece.lastrect)
        
        cleanup = []
        for piece in self.animated: self.ClearRect(piece.lastrect)
        for piece in self.animated:            
            piece.time_passed = piece.time_passed + event.GetInterval()
            if piece.time_passed >= piece.time:
                piece.time_passed = piece.time
                cleanup.append(piece)
                continue
            
            factor = 1.0*piece.time_passed/piece.time
            newx = piece.origin[0] + factor*(piece.target[0] - piece.origin[0])
            newy = piece.origin[1] + factor*(piece.target[1] - piece.origin[1])
            newrect = wx.Rect(newx*sz, newy*sz, sz, sz)
            self.DrawAtRect(self.pieces[piece.piecetype], newrect)
            piece.lastrect = newrect
        
        for piece in cleanup:
            if self.dragged_piece:
                if piece.target == self.dragged_piece.origin:
                    self.ReleaseMouse()
                    self.ClearRect(self.dragged_piece.lastrect)
                    self.dragged_piece = None
            self.PutPieceOnBoard(piece.promote_to, piece.target)
            self.animated.remove(piece)
            
        if self.dragged_piece:
            self.DrawAtRect(self.pieces[self.dragged_piece.piecetype], self.dragged_piece.lastrect)
            
        dc.SelectObject(wx.NullBitmap)
        if len(self.animated) == 0: self.animationTimer.Stop()
        
    def DrawAtRect(self, bitmap, rect):
        """ Draws a bitmap to the onscreen buffer. """
        
        dc = wx.MemoryDC()
        dc.SelectObject(self.onscreen)
        (x,y,w,h) = rect.Get()
        dc.DrawBitmap(bitmap, x, y, True)
        dc.SelectObject(wx.NullBitmap)
        self.RefreshRect(rect, False)

    def ClearRect(self, rect):
        """ Redraws a piece of the onscreen buffer with the corresponding part
            of the static board buffer. """
            
        dc = wx.MemoryDC()
        dc.SelectObject(self.onscreen)
        dirtyfield = self.board.GetSubBitmap(rect)
        (x,y,w,h) = rect.Get()
        dc.DrawBitmap(dirtyfield,x,y,False)
        dc.SelectObject(wx.NullBitmap)
        self.RefreshRect(rect, False)
        
    def FinishAnimations(self):
        self.animationTimer.Stop()
        for piece in self.animated:
            self.PutPieceOnBoard(piece.piecetype, piece.target)
            self.animated.remove(piece)
        
    def DoTextMove(self, movetext, movetype = ChessBoard.NORMAL_MOVE, animated = True):
        """ Takes a textmove and processes it onscreen. Does not check for move
            validity. Supports move animation. """
            
        origin = self.GetScreenPosFromAN(movetext[0:2])
        target = self.GetScreenPosFromAN(movetext[2:])        
            
        if not self.flipped: piecetype = self.position[origin[1]][origin[0]]
        else: piecetype = self.position[7-origin[1]][7-origin[0]]
        
        if self.dragged_piece:
            if origin == self.dragged_piece.origin:
                piecetype = self.dragged_piece.piecetype
                self.ReleaseMouse()
                self.ClearRect(self.dragged_piece.lastrect)
                self.dragged_piece = None
        
        self.PutPieceOnBoard('.', origin)            
        if animated:
            sz = self.squaresize
            piecerect = wx.Rect(origin[0]*sz, origin[1]*sz, sz, sz)
            self.DrawAtRect(self.pieces[piecetype],piecerect)
            animpiece = AnimatedPiece(piecetype,origin,target,300,piecerect)
            animpiece.lastrect = piecerect
            self.animated.append(animpiece)
            if len(self.animated) == 1: self.animationTimer.Start(20, False)
        else:
            self.PutPieceOnBoard(piecetype, target)
        if movetype: self.HandleSpecials(movetype, origin, target, animated)
        if self.dragged_piece:
            if target == self.dragged_piece.origin:
                self.ReleaseMouse()
                self.ClearRect(self.dragged_piece.lastrect)
                self.dragged_piece = None
        
    
    def PutPieceOnBoard(self, piecetype, field):
        """ Updates the board position, draws the piece onto the static board
            buffer, and also updates the onscreen buffer. """
            
        sz = self.squaresize
        (x,y) = field
        dc = wx.MemoryDC()
        
        targetrect = wx.Rect(x*sz,y*sz,sz,sz)
        targetfield = self.field.GetSubBitmap(targetrect)

        dc.SelectObject(self.board)
        dc.DrawBitmap(targetfield, x*sz, y*sz, False)
        if piecetype != '.': dc.DrawBitmap(self.pieces[piecetype], x*sz, y*sz, True)
        
        dc.SelectObject(self.onscreen)
        dc.DrawBitmap(targetfield, x*sz, y*sz, False)
        if piecetype != '.': dc.DrawBitmap(self.pieces[piecetype], x*sz, y*sz, True)
        
        dc.SelectObject(wx.NullBitmap)
        if self.flipped: self.position[7-y][7-x] = piecetype
        else: self.position[y][x] = piecetype
        self.RefreshRect(targetrect, False)
       
    def SetPromotion(self, promote_to):
        """ Used to set the piece to promote a pawn to. Needs to be invoked
            when a promotion takes place, otherwise the piece will graphically
            remain a pawn. """
            
        self.promotion = promote_to

    def HandleSpecials(self, movetype, origin, target, animated):
        """ Takes care that 'special' moves like castling are handled properly."""
        if movetype == ChessBoard.EP_CAPTURE_MOVE: 
            self.PutPieceOnBoard('.', (target[0],origin[1]))
        elif movetype == ChessBoard.KING_CASTLE_MOVE:
            if self.GetANFromScreenPos(origin) == 'e1':
                self.DoTextMove('h1f1', animated=animated)
            else: 
                self.DoTextMove('h8f8', animated=animated)
        elif movetype == ChessBoard.QUEEN_CASTLE_MOVE:
            if self.GetANFromScreenPos(origin) == 'e1':
                self.DoTextMove('a1d1', animated=animated)
            else:
                self.DoTextMove('a8d8', animated=animated)
        elif movetype == ChessBoard.PROMOTION_MOVE:
            resolved = False
            for p in self.animated:
                if p.target == target:
                    p.promote_to = self.promotion
                    resolved = True
            if not resolved: self.PutPieceOnBoard(self.promotion, target)

    def MoveWasValid(self, movetype):
        """ Used to tell the class that a move that was created by drag-
            and-drop was processed and found valid. Processes the move 
            graphically and displays it on screen. """
        
        self.FinishAnimations()
        
        p = self.dragged_piece
        self.PutPieceOnBoard(p.piecetype, p.target)
        self.HandleSpecials(movetype, p.origin, p.target, True)        
        self.ClearRect(p.lastrect)
        self.dragged_piece = None
        
    def MoveWasInvalid(self):
        """ Used to tell the class that a move that was created by drag-
            and-drop was processed and found invalid. Replaces the dragged
            piece to its original position. """
            
        p = self.dragged_piece
        self.PutPieceOnBoard(p.piecetype, p.origin)
        self.ClearRect(p.lastrect)
        self.dragged_piece = None
        
    def OnMotion(self, event):
        """ Used to update the position of the piece currently being dragged. """
        
        if event.Dragging() and event.LeftIsDown() and self.dragged_piece:
            sz = self.squaresize
            (clickedx,clickedy) = event.GetPositionTuple()
            clickedx = max(min(clickedx, 7.5*sz),sz/2)
            clickedy = max(min(clickedy, 7.5*sz),sz/2)
            newrect = wx.Rect(clickedx-sz/2, clickedy-sz/2, sz, sz)
            
            self.ClearRect(self.dragged_piece.lastrect)
            self.DrawAtRect(self.pieces[self.dragged_piece.piecetype],newrect)
            self.dragged_piece.lastrect = newrect
    
    def pickupPiece(self, clickpos):
        """ Used when a piece is clicked to start dragging it around. """
        
        sz = self.squaresize
        (clickedx,clickedy) = clickpos
        x = int(clickedx/sz)
        y = int(clickedy/sz)
        if self.flipped: piecetype = self.position[7-y][7-x]
        else: piecetype = self.position[y][x]
        if (piecetype != '.') & (self.dragged_piece == None):
            self.PutPieceOnBoard('.', (x,y))
            clickedx = max(min(clickedx, 7.5*sz),sz/2)
            clickedy = max(min(clickedy, 7.5*sz),sz/2)            
            piecerect = wx.Rect(clickedx-sz/2, clickedy-sz/2, sz, sz)
            self.DrawAtRect(self.pieces[piecetype], piecerect)
            self.dragged_piece = MovingPiece(piecetype, (x,y), piecerect)            
            self.CaptureMouse()

    def OnLeftDown(self, event):
        """ Handles a left mouse click. """
        
        self.pickupPiece(event.GetPositionTuple())

    def OnLeftUp(self, event):
        """ Checks whether a piece is dragged, and processes the drop part of
            the drag-and-drop by sending a piece_moved event. This event should
            be picked up by the controller to check for move validity. Use the
            MoveWasValid and MoveWasInvalid methods afterwards. """
            
        if self.dragged_piece:
            self.ReleaseMouse()
            
            (clickedx,clickedy) = event.GetPositionTuple()
            sz = self.squaresize
            x = min(max(int(clickedx/sz), 0), 7)
            y = min(max(int(clickedy/sz), 0), 7)
            (orix, oriy) = self.dragged_piece.origin
            self.dragged_piece.target = (x,y)
            
            move = self.GetANFromScreenPos((orix, oriy)) + self.GetANFromScreenPos((x,y))
            evt = PieceMovedEvent(myEVT_PIECE_MOVED, self.GetId())
            evt.SetMove(move)
            self.GetEventHandler().ProcessEvent(evt)
        
    def setupFromFEN(self, fen='rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'):
        """ Sets up the board from standard FEN notation. Only really needs the
            first field of the FEN string. Updates the position, as well as
            the board and onscreen buffers. """
            
        x = 0
        y = 0
        postext = fen.split()
        postext = postext[0]
        lines = postext.split('/')
        position = []
        if len(lines) != 8: error('Invalid FEN')
        for i in range(8):
            line = []
            for c in lines[i]:
                if c in "kqrnbpKQRNBP":
                    line.append(c)
                elif c in "12345678":
                    for j in range(int(c)):
                        line.append('.')
                else:
                    error('Invalid FEN')
            if len(line) != 8: error('Invalid FEN')
            position.append(line)
        return self.setupFromPosition(position)

    def setupFromPosition(self, position):
        """ Sets up the board from a position (double character array 
            containing an entry for each field). Updates the board and onscreen
            buffers as well. """
            
        x = 0
        y = 0
        sz = self.squaresize
        
        board = copyBitmap(self.field)
        dc = wx.MemoryDC()
        dc.SelectObject(board)
        
        for x in range(8):
            for y in range(8):
                if self.flipped: piece = position[7-y][7-x]
                else: piece = position[y][x]
                if piece in "kqrnbpKQRNBP":
                    dc.DrawBitmap(self.pieces[piece],sz*x,sz*y,True)
        
        dc.SelectObject(wx.NullBitmap)
        self.position = position                
        return board        
        
    def scalePieces(self, originals):
        """ Scale the piece images to the currently needed size. """
        
        scaledPieces = {}
        for piecetype, image in originals.iteritems():
            scaledPieces[piecetype] = image.Scale(self.squaresize,self.squaresize).ConvertToBitmap()
        return scaledPieces            
        
    def loadPieces(self, piecedir = "Data/piecesets/igor"):
        """ Loads the pieces images from the harddrive. Expects the images to
            be in .png format. """
        
        pieces = {}
        for piecetype, piecefile in self.piece_files.iteritems():
            filename = os.path.join(piecedir, self.piece_files[piecetype]+'.png')
            image = wx.Image(filename ,wx.BITMAP_TYPE_PNG)
            pieces[piecetype] = image
        return pieces
    
    def OnIdle(self, event):
        """ Rebuilds the screen if this is requested. Needed primarilly for 
            dynamically resizing the board. """
            
        if self.rebuild:
            (x,y) = self.GetClientSize()
            sqsize = max(min(x/8, y/8), 8)
            newsize = (8*sqsize,8*sqsize)
            self.SetClientSize(newsize)
            self.field = self.makeField(newsize)
            self.pieces = self.scalePieces(self.originalPieces)   
            self.board = self.setupFromPosition(self.position)
            self.onscreen = copyBitmap(self.board)
            self.rebuild = False
    
    def OnSize(self, event):
        """ Handles a resize event, by requesting the board to be rebuilt. """
        
        self.rebuild = True;
    
    def OnPaint(self, event):
        """ Paints the onscreen buffer to the screen. """
        
        dc = wx.BufferedPaintDC(self,self.onscreen)
    
    def makeField(self, size):
        """ Creates a bitmap of an 8x8 empty chessboard. """
        
        self.light_square = self.makeSquare(size, (255,206,158))
        self.dark_square = self.makeSquare(size, (209,139,71))
        field = wx.EmptyBitmap(*size)
        dc = wx.MemoryDC()
        dc.SelectObject(field)
        for x in range(4):
            for y in range(4):
                dc.DrawBitmap(self.light_square,self.squaresize*2*x,self.squaresize*2*y,False)
                dc.DrawBitmap(self.dark_square,self.squaresize*(2*x+1),self.squaresize*2*y,False)
                dc.DrawBitmap(self.light_square,self.squaresize*(2*x+1),self.squaresize*(2*y+1),False)
                dc.DrawBitmap(self.dark_square,self.squaresize*(2*x),self.squaresize*(2*y+1),False)
        dc.SelectObject(wx.NullBitmap)
        return field
        
    def makeSquare(self, size, color):
        """ Creates a bitmap representing an empty square on the chessboard. """

        (x, y) = size
        self.squaresize = round(min(x/8-0.5, y/8-0.5))
        square = wx.EmptyBitmap(self.squaresize, self.squaresize)
        dc = wx.MemoryDC()
        dc.SelectObject(square)
        dc.SetBackground(wx.Brush(color))
        dc.Clear()
        dc.SelectObject(wx.NullBitmap)
        return square
    
class MovingPiece(object):
    """ Class to hold data for a piece being dragged around. """
    
    def __init__(self, piecetype, origin, startrect):
        self.piecetype = piecetype
        self.origin = origin
        self.target = None
        self.lastrect = startrect
        self.promote_to = piecetype
        
class AnimatedPiece(MovingPiece):
    """ Class to hold data for a piece which is currently being animated. """
    
    def __init__(self,piecetype,origin,target,millis,startrect):
        MovingPiece.__init__(self,piecetype,origin,startrect)
        self.target = target
        self.time = millis
        self.time_passed = 0
        self.promote_to = piecetype

myEVT_PIECE_MOVED = wx.NewEventType()
EVT_PIECE_MOVED = wx.PyEventBinder(myEVT_PIECE_MOVED, 1)
class PieceMovedEvent(wx.PyCommandEvent):
    """ Event that is sent when a piece is dragged and dropped onto the field. 
        This event should be picked up by the controller to verify the move
        validity. The controller should then call either the MoveWasValid or 
        the MoveWasInvalid method of the ChessView class. """
    
    def __init__(self, evtType, id):
        wx.PyCommandEvent.__init__(self, evtType, id)
        self.move = None
    
    def GetMove(self):
        return self.move
    
    def SetMove(self, move):
        self.move = move


class PromotionDialog(wx.Dialog):
    """ Dialog that asks which piece the user wishes to promote his pawn to."""
    
    def __init__(self, parent, pieces, piecetypes):
        wx.Dialog.__init__(self, parent, wx.ID_ANY, "Select which piece to promote to:")
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.button_ids = {}
        for type in piecetypes:            
            button = wx.BitmapButton(self, wx.ID_ANY, pieces[type], pos=(0,0))
            self.button_ids[button.GetId()] = type
            self.Bind(wx.EVT_BUTTON, self.Close, button)
            sizer.Add(button)
        self.SetSizer(sizer)
        self.Fit()
        self.selection = piecetypes[0]
    
    def Close(self, event):
        self.selection = self.button_ids[event.GetId()]
        wx.Dialog.Close(self)        
        
    def ShowModal(self):
        wx.Dialog.ShowModal(self)
        return self.selection

if __name__ == '__main__':
    def onMoved(event):
        game.MoveWasInvalid()
    app = wx.PySimpleApp()
    frame = wx.Frame(None, wx.ID_ANY, "Chess View test (rejects all moves) ")
    game = ChessView(frame, (520,520))
    frame.Bind(EVT_PIECE_MOVED, onMoved)
    frame.Fit()
    frame.Show()
##    wx.FutureCall(2000,game.DoTextMove,"e2e4")
    app.MainLoop()
